const SEA = require('./sea');
const Gun = SEA.Gun;
// After we have a GUN extension to make user registration/login easy, we then need to handle everything else.

// We do this with a GUN adapter, we first listen to when a gun instance is created (and when its options change)
Gun.on('opt', function(at) {
  if (!at.sea) {
    // only add SEA once per instance, on the "at" context.
    at.sea = { own: {} };
    at.on('in', security, at); // now listen to all input data, acting as a firewall.
    at.on('out', signature, at); // and output listeners, to encrypt outgoing data.
    at.on('node', each, at);
  }
  this.to.next(at); // make sure to call the "next" middleware adapter.
});

// Alright, this next adapter gets run at the per node level in the graph database.
// This will let us verify that every property on a node has a value signed by a public key we trust.
// If the signature does not match, the data is just `undefined` so it doesn't get passed on.
// If it does match, then we transform the in-memory "view" of the data into its plain value (without the signature).
// Now NOTE! Some data is "system" data, not user data. Example: List of public keys, aliases, etc.
// This data is self-enforced (the value can only match its ID), but that is handled in the `security` function.
// From the self-enforced data, we can see all the edges in the graph that belong to a public key.
// Example: ~ASDF is the ID of a node with ASDF as its public key, signed alias and salt, and
// its encrypted private key, but it might also have other signed values on it like `profile = <ID>` edge.
// Using that directed edge's ID, we can then track (in memory) which IDs belong to which keys.
// Here is a problem: Multiple public keys can "claim" any node's ID, so this is dangerous!
// This means we should ONLY trust our "friends" (our key ring) public keys, not any ones.
// I have not yet added that to SEA yet in this alpha release. That is coming soon, but beware in the meanwhile!
function each(msg) {
  // TODO: Warning: Need to switch to `gun.on('node')`! Do not use `Gun.on('node'` in your apps!
  // NOTE: THE SECURITY FUNCTION HAS ALREADY VERIFIED THE DATA!!!
  // WE DO NOT NEED TO RE-VERIFY AGAIN, JUST TRANSFORM IT TO PLAINTEXT.
  var to = this.to,
    //vertex = msg.$._.put,
    c = 0,
    d;
  Gun.node.is(msg.put, function(val, key, node) {
    c++; // for each property on the node
    // TODO: consider async/await use here...
    SEA.verify(val, false, function(data) {
      c--; // false just extracts the plain data.
      node[key] = val = data; // transform to plain value.
      if (d && !c && (c = -1)) {
        to.next(msg);
      }
    });
  });
  d = true;
  if (d && !c) {
    to.next(msg);
  }
  return;
}

// signature handles data output, it is a proxy to the security function.
function signature(msg) {
  if (msg.user) {
    return this.to.next(msg);
  }
  var ctx = this.as;
  msg.user = ctx.user;
  security.call(this, msg);
}

function relpub(s) {
  if (!s) {
    return;
  }
  s = s.split('~');
  if (!s || !(s = s[1])) {
    return;
  }
  s = s.split('.');
  if (!s || 2 > s.length) {
    return;
  }
  s = s.slice(0, 2).join('.');
  return s;
}

// okay! The security function handles all the heavy lifting.
// It needs to deal read and write of input and output of system data, account/public key data, and regular data.
// This is broken down into some pretty clear edge cases, let's go over them:
function security(msg) {
  var at = this.as,
    //sea = at.sea,
    to = this.to;
  if (msg.get) {
    // if there is a request to read data from us, then...
    var soul = msg.get['#'];
    if (soul) {
      // for now, only allow direct IDs to be read.
      if (typeof soul !== 'string') {
        return to.next(msg);
      } // do not handle lexical cursors.
      if ('alias' === soul) {
        // Allow reading the list of usernames/aliases in the system?
        return to.next(msg); // yes.
      } else if ('~@' === soul.slice(0, 2)) {
        // Allow reading the list of public keys associated with an alias?
        return to.next(msg); // yes.
      } else {
        // Allow reading everything?
        return to.next(msg); // yes // TODO: No! Make this a callback/event that people can filter on.
      }
    }
  }
  if (msg.put) {
    // potentially parallel async operations!!!
    var check = {},
      each = {},
      u;
    each.node = function(node, soul) {
      if (Gun.obj.empty(node, '_')) {
        return (check['node' + soul] = 0);
      } // ignore empty updates, don't reject them.
      Gun.obj.map(node, each.way, { soul: soul, node: node });
    };
    each.way = function(val, key) {
      var soul = this.soul,
        node = this.node,
        tmp;
      if ('_' === key) {
        return;
      } // ignore meta data
      if ('~@' === soul) {
        // special case for shared system data, the list of aliases.
        each.alias(val, key, node, soul);
        return;
      }
      if ('~@' === soul.slice(0, 2)) {
        // special case for shared system data, the list of public keys for an alias.
        each.pubs(val, key, node, soul);
        return;
      }
      if (
        '~' === soul.slice(0, 1) &&
        2 === (tmp = soul.slice(1)).split('.').length
      ) {
        // special case, account data for a public key.
        each.pub(val, key, node, soul, tmp, msg.user);
        return;
      }
      each.any(val, key, node, soul, msg.user);
      return;
      //return each.end({ err: 'No other data allowed!' });
    };

    // eslint-disable-next-line no-unused-vars
    each.alias = function(val, key, node, soul) {
      // Example: {_:#~@, ~@alice: {#~@alice}}
      if (!val) {
        return each.end({ err: 'Data must exist!' });
      } // data MUST exist
      if ('~@' + key === Gun.val.link.is(val)) {
        return (check['alias' + key] = 0);
      } // in fact, it must be EXACTLY equal to itself
      each.end({ err: 'Mismatching alias.' }); // if it isn't, reject.
    };
    each.pubs = function(val, key, node, soul) {
      // Example: {_:#~@alice, ~asdf: {#~asdf}}
      if (!val) {
        return each.end({ err: 'Alias must exist!' });
      } // data MUST exist
      if (key === Gun.val.link.is(val)) {
        return (check['pubs' + soul + key] = 0);
      } // and the ID must be EXACTLY equal to its property
      each.end({ err: 'Alias must match!' }); // that way nobody can tamper with the list of public keys.
    };
    each.pub = function(val, key, node, soul, pub, user) {
      // Example: {_:#~asdf, hello:SEA{'world',fdsa}}
      if ('pub' === key) {
        if (val === pub) {
          return (check['pub' + soul + key] = 0);
        } // the account MUST match `pub` property that equals the ID of the public key.
        return each.end({ err: 'Account must match!' });
      }
      check['user' + soul + key] = 1;
      if (user && (user = user._) && user.sea && pub === user.pub) {
        //var id = Gun.text.random(3);
        SEA.sign(val, user.sea, function(data) {
          var rel;
          if (u === data) {
            return each.end({ err: SEA.err || 'Pub signature fail.' });
          }
          if ((rel = Gun.val.link.is(val))) {
            (at.sea.own[rel] = at.sea.own[rel] || {})[pub] = true;
          }
          node[key] = data;
          check['user' + soul + key] = 0;
          each.end({ ok: 1 });
        });
        // TODO: Handle error!!!!
        return;
      }

      // eslint-disable-next-line no-unused-vars
      SEA.verify(val, pub, function(data) {
        var rel;
        //tmp;
        if (u === data) {
          // make sure the signature matches the account it claims to be on.
          return each.end({ err: 'Unverified data.' }); // reject any updates that are signed with a mismatched account.
        }
        if ((rel = Gun.val.link.is(data)) && pub === relpub(rel)) {
          (at.sea.own[rel] = at.sea.own[rel] || {})[pub] = true;
        }
        check['user' + soul + key] = 0;
        each.end({ ok: 1 });
      });
    };

    each.any = function(val, key, node, soul, user) {
      var tmp, pub;
      if (!user || !(user = user._) || !(user = user.sea)) {
        if ((tmp = relpub(soul))) {
          check['any' + soul + key] = 1;
          SEA.verify(val, (pub = tmp), function(data) {
            var rel;
            if (u === data) {
              return each.end({ err: 'Mismatched owner on \'' + key + '\'.' });
            } // thanks @rogowski !
            if ((rel = Gun.val.link.is(data)) && pub === relpub(rel)) {
              (at.sea.own[rel] = at.sea.own[rel] || {})[pub] = true;
            }
            check['any' + soul + key] = 0;
            each.end({ ok: 1 });
          });
          return;
        }
        check['any' + soul + key] = 1;
        at.on('secure', function(msg) {
          this.off();
          check['any' + soul + key] = 0;
          if (at.opt.secure) {
            msg = null;
          }
          each.end(msg || { err: 'Data cannot be modified.' });
        }).on.on('secure', msg);
        //each.end({err: "Data cannot be modified."});
        return;
      }
      if (!(tmp = relpub(soul))) {
        if (at.opt.secure) {
          each.end({ err: 'Soul is missing public key at \'' + key + '\'.' });
          return;
        }
        if (val && val.slice && 'SEA{' === val.slice(0, 4)) {
          check['any' + soul + key] = 0;
          each.end({ ok: 1 });
          return;
        }
        //check['any'+soul+key] = 1;
        //SEA.sign(val, user, function(data){
        // if(u === data){ return each.end({err: 'Any signature failed.'}) }
        //  node[key] = data;
        check['any' + soul + key] = 0;
        each.end({ ok: 1 });
        //});
        return;
      }
      pub = tmp;
      if (pub !== user.pub) {
        each.any(val, key, node, soul);
        return;
      }
      /*var other = Gun.obj.map(at.sea.own[soul], function(v, p){
            if(user.pub !== p){ return p }
          });
          if(other){
            each.any(val, key, node, soul);
            return;
          }*/
      check['any' + soul + key] = 1;
      SEA.sign(val, user, function(data) {
        if (u === data) {
          return each.end({ err: 'My signature fail.' });
        }
        node[key] = data;
        check['any' + soul + key] = 0;
        each.end({ ok: 1 });
      });
    };
    each.end = function(ctx) {
      // TODO: Can't you just switch this to each.end = cb?
      if (each.err) {
        return;
      }
      if ((each.err = ctx.err) || ctx.no) {
        console.log('NO!', each.err, msg.put);

        // FIXME: Throw when not the expected error
        //if (e !== TODO) throw e;
        console.log(
          'If this error occured during normal operation validate it here',
          each.err
        );

        return;
      }
      if (!each.end.ed) {
        return;
      }
      if (
        Gun.obj.map(check, function(no) {
          if (no) {
            return true;
          }
        })
      ) {
        return;
      }
      to.next(msg);
    };
    Gun.obj.map(msg.put, each.node);
    each.end({ end: (each.end.ed = true) });
    return; // need to manually call next after async.
  }
  to.next(msg); // pass forward any data we do not know how to handle or process (this allows custom security protocols).
}
